﻿namespace Home.Test
{
    using Newtonsoft.Json;
    using Newtonsoft.Json.Linq;
    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.Globalization;
    using System.Linq;
    using System.Reflection;
    using System.Text;
    using System.Web;
    using System.Web.Security;

    public static class Helper
    {
        private const BindingFlags PropertyBindingFlags = BindingFlags.FlattenHierarchy | BindingFlags.GetProperty | BindingFlags.Instance | BindingFlags.Public;

        public const string GuidFormat = "N";

        public static class ContentType
        {
            public const string Json = "application/json";
            public const string JavaScript = "text/javascript";
        }

        public static class QQ
        {
            public static class DefaultExpiresDuration
            {
                public const int Authentication = 3600 * 24 * 180;
                public const int AuthorizationCode = 600;
                public const int AccessToken = 3600 * 2;
                public const int RefreshToken = 3600 * 24 * 30;
            }

            public const string CookiePath = "/qq";
            public const string DefaultOpenId = "qqopenid";
        }

        public static class Sina
        {
            public static class DefaultExpiresDuration
            {
                public const int Authentication = 3600 * 24 * 180;
                public const int AuthorizationCode = 3600;
                public const int AccessToken = 3600 * 2;
                public const int RefreshToken = 3600 * 24 * 30;
            }

            public const string CookiePath = "/sina";
            public const string DefaultOpenId = "sinaopenid";
        }

        public static class WeiXin
        {
            public static class DefaultExpiresDuration
            {
                public const int Authentication = 3600 * 24 * 180;
                public const int AuthorizationCode = 3600;
                public const int AccessToken = 3600 * 2;
                public const int RefreshToken = 3600 * 24 * 30;
            }

            public const string CookiePath = "/weixin";
            public const string DefaultOpenId = "weixinopenid";
        }

        public static class IdentityProvider
        {
            public const int CodeOk = 0;
            public const int CodeError = 20001;

            public static readonly IDictionary<int, string> Errors = new Dictionary<int, string>()
            {
                { CodeOk, "ok" },
                { CodeError, "error" },
            };
        }

        public static class Web
        {
            public static string ToQueryString(IDictionary<string, string> query)
            {
                Debug.Assert(query != null);

                var result = new StringBuilder();

                if (query.Count > 0)
                {
                    var first = query.ElementAt(0);
                    result.AppendFormat("{0}={1}", first.Key, HttpUtility.UrlEncode(first.Value));

                    for (int i = 1; i < query.Count; i++)
                    {
                        var item = query.ElementAt(i);
                        result.AppendFormat("&{0}={1}", item.Key, HttpUtility.UrlEncode(item.Value));
                    }
                }

                return result.ToString();
            }

            public static string ToQueryString(object query)
            {
                Debug.Assert(query != null);

                var dictionary = new Dictionary<string, string>();
                var properties = query.GetType().GetTypeInfo().GetProperties(PropertyBindingFlags);
                foreach (var property in properties)
                {
                    var value = property.GetValue(query, null);
                    var valueString = ConvertToString(value);
                    var attribute = property.GetCustomAttribute<NameAttribute>(false);
                    dictionary.Add(attribute == null ? property.Name : attribute.Name, valueString);
                }

                return ToQueryString(dictionary);
            }

            public static string ToUrl(string path, IDictionary<string, string> query)
            {
                Debug.Assert(path != null);
                Debug.Assert(query != null);

                var builder = new UriBuilder(path);
                builder.Query = ToQueryString(query);
                return builder.ToString();
            }

            public static string ToUrl(string path, object query)
            {
                Debug.Assert(path != null);
                Debug.Assert(query != null);

                var builder = new UriBuilder(path);
                builder.Query = ToQueryString(query);
                return builder.ToString();
            }

            public static string ToAbsolute(HttpContext context, string relativeOrAbsolute)
            {
                Debug.Assert(context != null);
                Debug.Assert(relativeOrAbsolute != null);

                return ToAbsoluteCore(context.Request.Url, relativeOrAbsolute);
            }

            public static string ToAbsolute(HttpContextBase context, string relativeOrAbsolute)
            {
                Debug.Assert(context != null);
                Debug.Assert(relativeOrAbsolute != null);

                return ToAbsoluteCore(context.Request.Url, relativeOrAbsolute);
            }

            private static string ToAbsoluteCore(Uri request, string relativeOrAbsolute)
            {
                var result = relativeOrAbsolute;

                var uri = new Uri(result, UriKind.RelativeOrAbsolute);
                if (!uri.IsAbsoluteUri)
                {
                    var builder = new UriBuilder(request.Scheme, request.Host, request.Port, result);
                    result = builder.ToString();
                }

                return result;
            }
        }

        public static class Crypto
        {
            public static string Protect<T>(T data)
                where T : class
            {
                Debug.Assert(data != null);

                var json = new JObject();
                var properties = typeof(T).GetTypeInfo().GetProperties(PropertyBindingFlags);
                foreach (var property in properties)
                {
                    var attribute = property.GetCustomAttribute<NameAttribute>(false);
                    var value = property.GetValue(data, null);
                    var valueString = ConvertToString(value);
                    json[attribute.Name] = valueString;
                }

                var jsonString = json.ToString(Formatting.None);
                var bytes = Encoding.UTF8.GetBytes(jsonString);
                bytes = MachineKey.Protect(bytes, "IdentityProviderWeb");
                return Convert.ToBase64String(bytes);
            }

            public static T Unprotect<T>(string code)
                where T : class, new()
            {
                Debug.Assert(code != null);

                var result = default(T);

                var bytes = default(byte[]);
                try
                {
                    bytes = Convert.FromBase64String(code);
                }
                catch (FormatException)
                {
                    goto EXIT;
                }

                bytes = MachineKey.Unprotect(bytes, "IdentityProviderWeb");
                result = new T();
                var properties = typeof(T).GetTypeInfo().GetProperties(PropertyBindingFlags);
                var jsonString = Encoding.UTF8.GetString(bytes);
                var json = JObject.Parse(jsonString);

                foreach (var item in json)
                {
                    var property = properties.First(p => p.GetCustomAttribute<NameAttribute>(false).Name == item.Key);
                    var value = item.Value.ToObject(property.PropertyType);
                    property.SetValue(result, value);
                }

            EXIT:
                return result;
            }
        }

        public static class Authentication
        {
            private const string CookieName = "IdentityProviderWeb_Auth";

            public static bool IsSignedIn(HttpContext context, IdentityProviderSource type)
            {
                Debug.Assert(context != null);
                Debug.Assert(Enum.IsDefined(typeof(IdentityProviderSource), type));

                return context.Request.Cookies.Get(CookieName) != null;
            }

            public static void SignIn<T>(HttpContextBase context, IdentityProviderSource type, T data)
                where T : class
            {
                Debug.Assert(context != null);
                Debug.Assert(Enum.IsDefined(typeof(IdentityProviderSource), type));
                Debug.Assert(data != null);

                var value = Crypto.Protect(data);
                context.Response.Cookies.Add(new HttpCookie(CookieName, value)
                {
                    Secure = false,
                    Path = GetCookiePath(type),
                    HttpOnly = true,
                    Expires = GetCookieExpires(type)
                });
            }

            public static void SignOff(HttpContextBase context, IdentityProviderSource type)
            {
                Debug.Assert(context != null);
                Debug.Assert(Enum.IsDefined(typeof(IdentityProviderSource), type));

                context.Response.Cookies.Add(new HttpCookie(CookieName)
                {
                    Secure = false,
                    Path = GetCookiePath(type),
                    HttpOnly = true,
                    Expires = DateTime.UtcNow.AddDays(-1)
                });
            }

            public static T GetSignInData<T>(HttpContext context, IdentityProviderSource type)
                where T : class, new()
            {
                Debug.Assert(context != null);
                Debug.Assert(Enum.IsDefined(typeof(IdentityProviderSource), type));

                var result = default(T);
                if (TryGetSignInData<T>(context, type, out result))
                {
                    return result;
                }

                throw new InvalidOperationException();
            }

            public static bool TryGetSignInData<T>(HttpContext context, IdentityProviderSource type, out T data)
                where T : class, new()
            {
                Debug.Assert(context != null);
                Debug.Assert(Enum.IsDefined(typeof(IdentityProviderSource), type));

                data = default(T);

                var cookie = context.Request.Cookies.Get(CookieName);
                if (cookie != null)
                {
                    data = Crypto.Unprotect<T>(cookie.Value);
                }

                return data != default(T);
            }

            private static string GetCookiePath(IdentityProviderSource type)
            {
                switch (type)
                {
                    case IdentityProviderSource.QQ: return QQ.CookiePath;
                    case IdentityProviderSource.Sina: return Sina.CookiePath;
                    case IdentityProviderSource.WeiXin: return WeiXin.CookiePath;
                    default: throw new InvalidOperationException();
                }
            }

            private static DateTime GetCookieExpires(IdentityProviderSource type)
            {
                switch (type)
                {
                    case IdentityProviderSource.QQ: return DateTime.UtcNow.AddSeconds(QQ.DefaultExpiresDuration.Authentication);
                    case IdentityProviderSource.Sina: return DateTime.UtcNow.AddSeconds(Sina.DefaultExpiresDuration.Authentication);
                    case IdentityProviderSource.WeiXin: return DateTime.UtcNow.AddSeconds(WeiXin.DefaultExpiresDuration.Authentication);
                    default: throw new InvalidOperationException();
                }
            }
        }

        public static string ToCodeString(Guid value)
        {
            return value.ToString(GuidFormat, CultureInfo.InvariantCulture);
        }

        private static string ConvertToString(object value)
        {
            if (value == null)
            {
                return null;
            }

            var type = value.GetType();
            if (type == typeof(string))
            {
                return (string)value;
            }

            var formattable = value as IFormattable;
            if (formattable != null)
            {
                if (type == typeof(Guid))
                {
                    return formattable.ToString(GuidFormat, CultureInfo.InvariantCulture);
                }
                else if (type == typeof(DateTime))
                {
                    return formattable.ToString("yyyyMMddhhmmss", CultureInfo.InvariantCulture);
                }
                else
                {
                    return formattable.ToString(null, CultureInfo.InvariantCulture);
                }
            }

            return value.ToString();
        }

        private static T ConvertToValue<T>(string value)
        {
            if (value == null)
            {
                return default(T);
            }

            var type = typeof(T);
            if (type == typeof(string))
            {
                return (T)(object)value;
            }

            if (type == typeof(Guid))
            {
                return (T)(object)Guid.ParseExact(value, GuidFormat);
            }
            else if (type == typeof(DateTime))
            {
                return (T)(object)DateTime.ParseExact(value, "yyyyMMddhhmmss", CultureInfo.InvariantCulture);
            }
            else if (typeof(IFormattable).GetTypeInfo().IsAssignableFrom(type))
            {
                return (T)(object)Convert.ChangeType(value, type, CultureInfo.InvariantCulture);
            }

            return (T)(object)Convert.ChangeType(value, type);
        }

        private static object ConvertToValue(string value, Type type)
        {
            if (value == null)
            {
                return null;
            }

            if (type == typeof(string))
            {
                return value;
            }

            if (type == typeof(Guid))
            {
                return Guid.ParseExact(value, GuidFormat);
            }
            else if (type == typeof(DateTime))
            {
                return DateTime.ParseExact(value, "yyyyMMddhhmmss", CultureInfo.InvariantCulture);
            }
            else if (typeof(IFormattable).GetTypeInfo().IsAssignableFrom(type))
            {
                return Convert.ChangeType(value, type, CultureInfo.InvariantCulture);
            }

            return Convert.ChangeType(value, type);
        }
    }
}